home *** CD-ROM | disk | FTP | other *** search
/ Total Network Tools 2002 / NextStepPublishing-TotalNetworkTools2002-Win95.iso / Archive / Misc Servers / Zope.exe / DEFAULT_HANDLER.PY < prev    next >
Encoding:
Python Source  |  2000-06-02  |  6.2 KB  |  249 lines

  1. # -*- Mode: Python; tab-width: 4 -*-
  2. #
  3. #    Author: Sam Rushing <rushing@nightmare.com>
  4. #    Copyright 1997 by Sam Rushing
  5. #                         All Rights Reserved.
  6. #
  7.  
  8. RCS_ID = '$Id: default_handler.py,v 1.4 2000/06/02 14:22:48 brian Exp $'
  9.  
  10. # standard python modules
  11. import os
  12. import regex
  13. import posixpath
  14. import stat
  15. import string
  16. import time
  17.  
  18. # medusa modules
  19. import http_date
  20. import http_server
  21. import mime_type_table
  22. import status_handler
  23. import producers
  24.  
  25. # from <lib/urllib.py>
  26. _quoteprog = regex.compile('%[0-9a-fA-F][0-9a-fA-F]')
  27. def unquote(s):
  28.     i = 0
  29.     n = len(s)
  30.     res = []
  31.     while 0 <= i < n:
  32.         j = _quoteprog.search(s, i)
  33.         if j < 0:
  34.             res.append(s[i:])
  35.             break
  36.         res.append(s[i:j] + chr(string.atoi(s[j+1:j+3], 16)))
  37.         i = j+3
  38.     return string.join (res, '')
  39.  
  40. # split a uri
  41. # <path>;<params>?<query>#<fragment>
  42. path_regex = regex.compile (
  43. #        path        params        query       fragment
  44.     '\\([^;?#]*\\)\\(;[^?#]*\\)?\\(\\?[^#]*\)?\(#.*\)?'
  45.     )
  46.  
  47. def split_path (path):
  48.     if path_regex.match (path) != len(path):
  49.         raise ValueError, "bad path"
  50.     else:
  51.         return map (lambda i,r=path_regex: r.group(i), range(1,5))
  52.  
  53. # This is the 'default' handler.  it implements the base set of
  54. # features expected of a simple file-delivering HTTP server.  file
  55. # services are provided through a 'filesystem' object, the very same
  56. # one used by the FTP server.
  57. #
  58. # You can replace or modify this handler if you want a non-standard
  59. # HTTP server.  You can also derive your own handler classes from
  60. # it.
  61. #
  62. # support for handling POST requests is available in the derived
  63. # class <default_with_post_handler>, defined below.
  64. #
  65.  
  66. from counter import counter
  67.  
  68. class default_handler:
  69.  
  70.     valid_commands = ['get', 'head']
  71.  
  72.     IDENT = 'Default HTTP Request Handler'
  73.  
  74.     # Pathnames that are tried when a URI resolves to a directory name
  75.     directory_defaults = [
  76.         'index.html',
  77.         'default.html'
  78.         ]
  79.  
  80.     default_file_producer = producers.file_producer
  81.  
  82.     def __init__ (self, filesystem):
  83.         self.filesystem = filesystem
  84.         # count total hits
  85.         self.hit_counter = counter()
  86.         # count file deliveries
  87.         self.file_counter = counter()
  88.         # count cache hits
  89.         self.cache_counter = counter()
  90.  
  91.     hit_counter = 0
  92.  
  93.     def __repr__ (self):
  94.         return '<%s (%s hits) at %x>' % (
  95.             self.IDENT,
  96.             self.hit_counter,
  97.             id (self)
  98.             )
  99.  
  100.     # always match, since this is a default
  101.     def match (self, request):
  102.         return 1
  103.  
  104.     # handle a file request, with caching.
  105.  
  106.     def handle_request (self, request):
  107.  
  108.         if request.command not in self.valid_commands:
  109.             request.error (400) # bad request
  110.             return
  111.  
  112.         self.hit_counter.increment()
  113.  
  114.         [path, params, query, fragment] = split_path (request.uri)
  115.  
  116.         # unquote path if necessary (thanks to Skip Montaro for pointing
  117.         # out that we must unquote in piecemeal fashion).
  118.         if '%' in path:
  119.             path = unquote (path)
  120.  
  121.         # strip off all leading slashes
  122.         while path and path[0] == '/':
  123.             path = path[1:]
  124.  
  125.         if self.filesystem.isdir (path):
  126.             if path and path[-1] != '/':
  127.                 request['Location'] = 'http://%s/%s/' % (
  128.                     request.channel.server.server_name,
  129.                     path
  130.                     )
  131.                 request.error (301)
  132.                 return
  133.  
  134.             # we could also generate a directory listing here,
  135.             # may want to move this into another method for that
  136.             # purpose
  137.             found = 0
  138.             if path and path[-1] != '/':
  139.                 path = path + '/'
  140.             for default in self.directory_defaults:
  141.                 p = path + default
  142.                 if self.filesystem.isfile (p):
  143.                     path = p
  144.                     found = 1
  145.                     break
  146.             if not found:
  147.                 request.error (404) # Not Found 
  148.                 return
  149.  
  150.         elif not self.filesystem.isfile (path):
  151.             request.error (404) # Not Found
  152.             return
  153.  
  154.         file_length = self.filesystem.stat (path)[stat.ST_SIZE]
  155.  
  156.         ims = get_header (IF_MODIFIED_SINCE, request.header)
  157.  
  158.         length_match = 1
  159.         if ims:
  160.             length = IF_MODIFIED_SINCE.group(4)
  161.             if length:
  162.                 try:
  163.                     length = string.atoi (length)
  164.                     if length != file_length:
  165.                         length_match = 0
  166.                 except:
  167.                     pass
  168.  
  169.         ims_date = 0
  170.  
  171.         if ims:
  172.             ims_date = http_date.parse_http_date (ims)
  173.  
  174.         try:
  175.             mtime = self.filesystem.stat (path)[stat.ST_MTIME]
  176.         except:
  177.             request.error (404)
  178.             return
  179.  
  180.         if length_match and ims_date:
  181.             if mtime <= ims_date:
  182.                 request.reply_code = 304
  183.                 request.done()
  184.                 self.cache_counter.increment()
  185.                 return
  186.         try:
  187.             file = self.filesystem.open (path, 'rb')
  188.         except IOError:
  189.             request.error (404)
  190.             return
  191.  
  192.         request['Last-Modified'] = http_date.build_http_date (mtime)
  193.         request['Content-Length'] = file_length
  194.         self.set_content_type (path, request)
  195.  
  196.         if request.command == 'get':
  197.             request.push (self.default_file_producer (file))
  198.  
  199.         self.file_counter.increment()
  200.         request.done()
  201.  
  202.     def set_content_type (self, path, request):
  203.         ext = string.lower (get_extension (path))
  204.         if mime_type_table.content_type_map.has_key (ext):
  205.             request['Content-Type'] = mime_type_table.content_type_map[ext]
  206.         else:
  207.             # TODO: test a chunk off the front of the file for 8-bit
  208.             # characters, and use application/octet-stream instead.
  209.             request['Content-Type'] = 'text/plain'
  210.  
  211.     def status (self):
  212.         return producers.simple_producer (
  213.             '<li>%s' % status_handler.html_repr (self)
  214.             + '<ul>'
  215.             + '  <li><b>Total Hits:</b> %s'            % self.hit_counter
  216.             + '  <li><b>Files Delivered:</b> %s'    % self.file_counter
  217.             + '  <li><b>Cache Hits:</b> %s'            % self.cache_counter
  218.             + '</ul>'
  219.             )
  220.  
  221. ACCEPT = regex.compile ('Accept: \(.*\)', regex.casefold)
  222.  
  223. # HTTP/1.0 doesn't say anything about the "; length=nnnn" addition
  224. # to this header.  I suppose it's purpose is to avoid the overhead
  225. # of parsing dates...
  226. IF_MODIFIED_SINCE = regex.compile (
  227.     'If-Modified-Since: \([^;]+\)\(\(; length=\([0-9]+\)$\)\|$\)',
  228.     regex.casefold
  229.     )
  230.  
  231. USER_AGENT = regex.compile ('User-Agent: \(.*\)', regex.casefold)
  232.  
  233. boundary_chars = "A-Za-z0-9'()+_,./:=?-"
  234.  
  235. CONTENT_TYPE = regex.compile (
  236.     'Content-Type: \([^;]+\)\(\(; boundary=\([%s]+\)$\)\|$\)' % boundary_chars,
  237.     regex.casefold
  238.     )
  239.  
  240. get_header = http_server.get_header
  241.  
  242. def get_extension (path):
  243.     dirsep = string.rfind (path, '/')
  244.     dotsep = string.rfind (path, '.')
  245.     if dotsep > dirsep:
  246.         return path[dotsep+1:]
  247.     else:
  248.         return ''
  249.